// -----------------------------
// Author: 		Dhaenens Gert	
// Creation: 	2/22/2015 12:39:01 PM				
// -----------------------------

#include "TextureLoader.h"

#include "Engine.h"
#include "GraphicsManager.h"
#include "JobManager.h"

#include "FileManager.h"
#include "FilePackage.h"
#include "File.h"

#include "ITexture.h"
#include "IMutex.h"
#include "IPlatform.h"

#include "PlatformProvider.h"

#include "Path.h"

#define X32TOX8(x)		((Byte) (x * 255.0f))

namespace Flow
{

	IMutex* TextureLoader::s_LibPNGLock = nullptr;

	void TextureLoader::LoadLock()
	{
		s_LibPNGLock = PlatformProvider::QueryInterface<IMutex>("Mutex");
		ASSERT(s_LibPNGLock);
	}
	void TextureLoader::UnloadLock()
	{
		s_LibPNGLock->Release();
	}

	TextureLoader::TextureLoader(IGraphicsDevice* lpDevice, IMutex* lpGraphicsMutex, const tstring& file)
		: m_lpGraphicsDevice(lpDevice)
		, m_lpGraphicsMutex(lpGraphicsMutex)
		, m_lpTexture(nullptr)
		, m_FilePath(file)
		, m_lpFile(nullptr)
	{
	}

	TextureLoader::~TextureLoader()
	{
	}

	ITexture* TextureLoader::Load()
	{
		string str = string(m_FilePath.begin(), m_FilePath.end());
		Log::WriteLine("Loading Texture \"%s\".", str.c_str());

		m_lpTexture = PlatformProvider::QueryInterface<ITexture>("Texture");
		ASSERT(m_lpTexture);

		Engine::Jobs->QueueJob(this);

		return m_lpTexture;
	}
	void TextureLoader::Run()
	{
		string str = string(m_FilePath.begin(), m_FilePath.end());
		Log::WriteLine("Reading Texture Data from file \"%s\".", str.c_str());

		// Find the Package
		FilePackage* lpPackage = Engine::Files->FindPackageForFile(m_FilePath);
		ASSERT(lpPackage);
		if (!lpPackage)
		{
			Log::WriteLine("Could not find package for file \"%s\".", str.c_str());
			return;
		}
		tstring packageName = lpPackage->GetName();
		string pstr = string(packageName.begin(), packageName.end());
		Log::WriteLine("Found package \"%s\" for file \"%s\".", pstr.c_str(), str.c_str());

		// Get the File
		m_lpFile = lpPackage->GetFile(m_FilePath);
		ASSERT(m_lpFile);

		// Read the Data
		UInt32 width = 0;
		UInt32 height = 0;
		TextureFormat::Enum format = TextureFormat::Unknown;

		s_LibPNGLock->Lock();
		void* lpData = ReadPNGData(width, height, format);
		s_LibPNGLock->Unlock();

		ASSERT(width > 0 && height > 0 && format != TextureFormat::Unknown && lpData != nullptr);

		// Create the Texture
		Log::WriteLine("Creating Texture (%ux%u %s)", width, height, TextureFormat::ToString(format).c_str());
		
		m_lpGraphicsMutex->Lock();
		Bool bResult = m_lpTexture->Create(m_lpGraphicsDevice, width, height, format, lpData);
		m_lpGraphicsMutex->Unlock();

		ASSERT(bResult != false);
		if (!bResult)
			Log::WriteLine("Could not create Texture.");

		if (lpData)
			free(lpData);

		Log::WriteLine("Finished loading Texture \"%s\".", str.c_str());
	}

	void* TextureLoader::ReadPNGData(UInt32& out_Width, UInt32& out_Height, TextureFormat::Enum& out_Format)
	{
		// Open the File
		m_lpFile->Open();

		// Read the signature
		Byte pngSig[8];
		m_lpFile->Read(pngSig, 8);
		
		//// Make sure that the signature is correct
		if (png_sig_cmp(pngSig, 0, 8) != 0)
		{
			ASSERT(false && "Invalid PNG Signature");
			return nullptr;
		}

		// Create the PNG Struct
		png_structp pngPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
		if (!pngPtr)
		{
			ASSERT(false && "Could not create PNG Struct");
			return nullptr;
		}

		// Create the PNG Info Struct
		png_infop infoPtr = png_create_info_struct(pngPtr);
		if (!infoPtr)
		{
			ASSERT(false && "Could not create PNG Info Struct");
			return false;
		}

		// Set the read function
		png_set_sig_bytes(pngPtr, 8);
		png_set_read_fn(pngPtr, (png_voidp) this, TextureLoader::readUserData);

		// Read the info
		png_read_info(pngPtr, infoPtr);

		// Determine the dimensions
		out_Width = png_get_image_width(pngPtr, infoPtr);
		out_Height = png_get_image_height(pngPtr, infoPtr);
		ASSERT(out_Width > 0 && out_Height > 0);

		// Determine the format
		// And set the conversion flags if not valid
		UInt32 bitsPerChannel = png_get_bit_depth(pngPtr, infoPtr);
		UInt32 channels = png_get_channels(pngPtr, infoPtr);
		UInt32 colorType = png_get_color_type(pngPtr, infoPtr);
		switch (colorType)
		{
		case PNG_COLOR_TYPE_PALETTE:
			png_set_palette_to_rgb(pngPtr);
			channels = 3;
			break;
		case PNG_COLOR_TYPE_GRAY:
			if (bitsPerChannel < 8)
				png_set_expand_gray_1_2_4_to_8(pngPtr);
			png_set_gray_to_rgb(pngPtr);
			bitsPerChannel = 8;
			channels = 3;
			break;
		}

		// If we have transparency, convert to alpha channel
		if (png_get_valid(pngPtr, infoPtr, PNG_INFO_tRNS))
		{
			png_set_tRNS_to_alpha(pngPtr);
			++channels;
		}

		if (bitsPerChannel == 16) // Convert to 8
			png_set_strip_16(pngPtr);

		UInt32 bpp = (bitsPerChannel * channels);
		ASSERT(bpp == 24 || bpp == 32 || bpp == 96 || bpp == 128);
		if (bpp != 24 && bpp != 32 && bpp != 96 && bpp != 128)
		{
			Log::WriteLine("Unsupported Bits per Pixel: %u", bpp);
			return nullptr;
		}
		
		// Allocate the buffer
		Size_T bufferSize = out_Width * out_Height * (bpp / 8);
		Byte* lpData = reinterpret_cast<Byte*>(malloc(bufferSize));
		memset(lpData, 0, bufferSize);

		// Build the row pointers
		Byte** lpRows = new Byte*[out_Height];
		UInt32 stride = (out_Width * (bpp / 8));
		for (UInt32 i = 0; i < out_Height; ++i)
			lpRows[i] = (Byte*)(lpData + (i * stride));

		// Read the image
		png_read_image(pngPtr, (png_bytepp) lpRows);

		// cleanup
		png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
		delete[] lpRows;

		// Determine if we need to convert
		if (bpp == 24) // R8B8G8
			lpData = ConvertR8B8G8(lpData, out_Width, out_Height);
		else if (bpp == 96) // R32B32G32
			lpData = ConvertR32B32G32(lpData, out_Width, out_Height);
		else if (bpp == 128) // R32B32G32A32
			lpData = ConvertR32B32G32A32(lpData, out_Width, out_Height);

		// By now we should be sure that it's R8B8G8A8
		out_Format = TextureFormat::R8B8G8A8;

		// Close the File
		m_lpFile->Close();

		return reinterpret_cast<void*>(lpData);
	}

	void TextureLoader::readUserData(png_structp pngPtr, png_bytep data, png_size_t length)
	{
		png_voidp userData = png_get_io_ptr(pngPtr);
		TextureLoader* lpLoader = reinterpret_cast<TextureLoader*>(userData);

		lpLoader->m_lpFile->Read(data, length);
	}

	Byte* TextureLoader::ConvertR8B8G8(Byte* lpData, UInt32 width, UInt32 height)
	{
		Byte* lpOut = reinterpret_cast<Byte*>(malloc(width * height * 32));
		Byte* lpSource = lpData;
		ASSERT(lpOut != nullptr);

		for (UInt32 y = 0; y < height; ++y)
		{
			for (UInt32 x = 0; x < width; ++x)
			{
				UInt32 sidx = ((y * width) + x) * 3;
				UInt32 tidx = ((y * width) + x) * 4;
				lpOut[tidx + 0] = lpSource[sidx + 0];
				lpOut[tidx + 1] = lpSource[sidx + 1];
				lpOut[tidx + 2] = lpSource[sidx + 2];
				lpOut[tidx + 3] = 255;
			}
		}

		free(lpData);
		return lpOut;
	}

	Byte* TextureLoader::ConvertR32B32G32(Byte* lpData, UInt32 width, UInt32 height)
	{
		Byte* lpOut = reinterpret_cast<Byte*>(malloc(width * height * 32));
		Float* lpSource = reinterpret_cast<Float*>(lpData);
		ASSERT(lpOut != nullptr);

		for (UInt32 y = 0; y < height; ++y)
		{
			for (UInt32 x = 0; x < width; ++x)
			{
				UInt32 sidx = ((y * width) + x) * 3;
				UInt32 tidx = ((y * width) + x) * 4;
				lpOut[tidx + 0] = X32TOX8(lpSource[sidx + 0]);
				lpOut[tidx + 1] = X32TOX8(lpSource[sidx + 1]);
				lpOut[tidx + 2] = X32TOX8(lpSource[sidx + 2]);
				lpOut[tidx + 3] = 255;
			}
		}

		free(lpData);
		return lpOut;
	}

	Byte* TextureLoader::ConvertR32B32G32A32(Byte* lpData, UInt32 width, UInt32 height)
	{
		Byte* lpOut = reinterpret_cast<Byte*>(malloc(width * height * 32));
		Float* lpSource = reinterpret_cast<Float*>(lpData);
		ASSERT(lpOut != nullptr);

		for (UInt32 y = 0; y < height; ++y)
		{
			for (UInt32 x = 0; x < width; ++x)
			{
				UInt32 idx = ((y * width) + x) * 4;
				lpOut[idx + 0] = X32TOX8(lpSource[idx + 0]);
				lpOut[idx + 1] = X32TOX8(lpSource[idx + 1]);
				lpOut[idx + 2] = X32TOX8(lpSource[idx + 2]);
				lpOut[idx + 3] = X32TOX8(lpSource[idx + 3]);
			}
		}

		free(lpData);
		return lpOut;
	}

}